Login
www.borland.com
Borland Developer Community Home
Community Home >Delphi> Web Technologies


Introducing the WebSnap Pack

Abstract:WebSnap is a lean, mean, web-serving machine! By Jimmy Tharpe.

The WebSnap framework is highly atomized thanks to the heavy use of interfaces. This makes it easy to create new implementations of common tasks. For example, WebSnap defines an interface (IWebUserList) for tracking user names, passwords, permissions, and display-names, and includes a basic implementation that tracks all that data in memory. So what if we want database functionality? Just re-implement the IWebUserList interface, as I did for the TDBWebUserList component that's a part of the WebSnap Pack, and it integrates right in with the rest of WebSnap!

So let's take a walk through techniques used in the free WebSnap Pack, which extends WebSnap by re-implementing some interfaces and takes advantage of some presently un-documented features.

TDBWebUserList

To create a database-enabled Web User List, the obvious answer was to implement the IWebUserList interface:

IWebUserList = interface
['{0877DEAF-AB5D-11D4-A503-00C04F6BB853}']
  function ValidateUser(Strings: TStrings): Variant;
  function CheckAccessRights(UserID: Variant; Rights: string): Boolean;
end;

Diving into WebSnap's implementation of this interface, I found one inadequacy: There's no way to get a user's display name! Was I scared? Did I give up? No! I simply did some extending by creating my own descended interface, IudWebUserList:

  IudWebUserList = interface(IWebUserList)
  ['{0C7E6E80-3F82-47C6-B37E-04BEA4FAEE4A}']
    function UserDisplayName(AUserID: variant): string;
  end;

Tada! Now we have an interface to implement, and it's still compatible with what WebSnap expects. Once this interface was in place, it only needed to be implemented, and it snapped right in to WebSnap, thanks to interfaces.

Adding Caching

I solicited feedback on TDBWebUserList from the community. Graham Colwell suggested that I add caching for logged-in users to speed things up. That way, it wouldn't be necessary to access and refresh the database each time data about the user (such as permission information) was needed. The solution seems pretty simple: just keep a list of logged-in users and access that list instead of the database. This brought up one problem, however: How can my component know when the user logs out so that it can delete him or her from the cache?

Back to diving through the documentation (also known as Borland's source code)! I couldn't find anything in the help, so I just started browsing WebSnap's source. Then I found the answer: INotifyWebActivate! The INotifyWebActivate interface does what you might expect: It lets its implementer be notified of activation and deactivation of the WebSnap application. INotifyWebActivate is defined as follows:

INotifyWebActivate = interface
['{CE18BE42-1358-11D4-ABF4-F18FFAD12B3C}']
  procedure NotifyActivate;
  procedure NotifyDeactivate;
end;

After implementing this undocumented interface, my component was magically notified of the activation and deactivation of my WebSnap application! Great! All it took, after discovering this interface, was a simple routine to determine whether the user is logged in or not, and to delete them from the cache if they're not.

TudEndUserSessionAdapter

The TudEndUserSessionAdapter component was added to the WebSnap Pack to take advantage of the TDBWebUserList's IudWebUserList interface and fix the EndUser.DisplayName bug (The TEndUserSessionAdapter displays the user name instead of the display-name).

To do this, I took a look at the TEndUserSessionAdapter and discovered that the the user name is stored in the Session object, but the display-name is not. Simple fix: Just set a new Session variable for the user name. When the display-name is requested from script, just read the session object and return the value:

function TudCustomEndUserSessionAdapter.GetUserName: string;
begin
  if WebContext <> nil then
    Result := WebContext.Session.Values[sUserName];
  if Result = '' then
    Result := UserID;
end;

Pretty simple, eh? See...this WebSnap stuff isn't too hard after all!

TudLoginFormAdapter

The TudLoginFormAdapter is by far the easiest addition to the WebSnap Pack. It simply takes the existing TLoginFormAdapter and publishes the NextPage property. Here is the full source code:

type
  TudLoginFormAdapter = class(TLoginFormAdapter)
  published
    property NextPage;
  end;

The NextPage property allows you to set where a user goes once he or she is logged in. For example, you may want to re-direct administrators to an administration page, or customers to a support page, depending on permissions. Here's a quick code example:

procedure TLoginPage.udLoginFormAdapterLogin(Sender: TObject; UserID: Variant);
begin
  if HomePage.DBWebUserList.CheckAccessRights(UserID, 'admin') then
    udLoginFormAdapter.NextPage := AdminPage.Name
  else if HomePage.DBWebUserList.CheckAccessRights(UserID, 'user') then
    udLoginFormAdapter.NextPage := UserPage.Name
  else
    udLoginFormAdapter.NextPage := VisitorPage.Name;
end;

Of course, because the NextPage property is published, you can set the value at design time.

TudStringsValuesList

The TudStringsValuesList was introduced for two reasons: to fix a Borland TStringsValuesList bug in which the OnPrepareStrings event is never called, and to provide access, in Delphi, to the current string in script iteration. Even though this required a complete rewrite, it was not very difficult. All it took was some copying and pasting, and a few minor changes to the code.

Fixing the bug. Fixing the OnPrepareStrings bug, was simple. Take a look at this snippet of the original Borland code for TStringsValuesList:

procedure TCustomStringsValuesList.SetStrings(const Value: TStrings);
begin
  FStrings.Assign(Value);
end;

function TCustomStringsValuesList.GetStrings: TStrings;
begin
  if not (csLoading in ComponentState) and not StringsPrepared then
    PrepareStrings;
  Result := FStrings;
end;

procedure TCustomStringsValuesList.PrepareStrings;
begin
  StringsPrepared := True;
  if Assigned(OnPrepareStrings) then
    OnPrepareStrings(Self);
end;

procedure TCustomStringsValuesList.ImplNotifyDeactivate;
begin
  inherited;
  StringsPrepared := False;
end;

The SetStrings and GetStrings methods are for the Strings property of the TStringsValuesList. If you look at the GetStrings method, you'll notice that it calls the PrepareStrings method only if the strings are not prepared and the component is not being loaded.

What is supposed to happen is, when the strings are needed for the first time, PrepareStrings should be called. When the request is deactivated, ImplNotifyDeactivate should set StringsPrepared back to false so that PrepareStrings would be called again on the next request. This doesn't happen, and here's why:

function TCustomStringsValuesList.ImplGetListName: string;
begin
  if FIndex < FStrings.Count then
    Result := FStrings.Names[FIndex]
  else
    Result := '';
end;

function TCustomStringsValuesList.ImplGetListValue: Variant;
var
  S: string;
begin
  if FIndex < FStrings.Count then
  begin
    S := FStrings.Names[FIndex];
    if S <> '' then
      Result := FStrings.Values[S]
    else
      Result := FStrings[FIndex]
  end
  else
    Result := Unassigned;
end;

function TCustomStringsValuesList.ImplNextIteration(
  var OwnerData: Pointer): Boolean;
begin
  Inc(FIndex);
  Result := FIndex < FStrings.Count;
end;

When the strings are needed in script, the code is accessing the internal FStrings variable, thus circumventing the GetStrings method! Whoops! To correct this problem, simply change the first call of FStrings from FStrings to Strings in each of the methods that access the strings.

Adding a CurrentIteration property. The purpose of the CurrentIteration property is so that you can tell, in your Delphi code, where your page's script is in its current iteration. To do that is simple. In the iteration implementation methods, just assign the returned string:

function TudStringsValuesList.ImplStartIterator(var OwnerData: Pointer):Boolean;
begin
  FIndex := 0;
  Result := FIndex < Strings.Count;
  if Result then
    FCurrentIteration := Strings[FIndex];
end;

function TudStringsValuesList.ImplNextIteration(var OwnerData:Pointer): Boolean;
begin
  Inc(FIndex);
  Result := FIndex < Strings.Count;
  if Result then
    FCurrentIteration := Strings[FIndex];  
end;

If the iteration is successful, result is true. And if the result is true, we set the CurrentIteration property to the value of that iteration.

Wrapping Up

Say it with me: WebSnap is a lean, mean, web-serving machine. When you get a chance, pop open the hood of WebSnap and see just what a fine piece of work Borland has done. And if you see room for improvement, feel free to contribute to the WebSnap Pack! It's there for the community.

Jimmy Tharpe is a skinny little 20-year-old kid who has nothing better to do that run his own company and play with Delphi all day long.


Add or View comments on this article

NOTE: The views and information expressed in this document represent those of its author(s) who are solely responsible for its content. Borland does not make or give any representation or warranty with respect to such content.

Article ID: 27905   Publish Date: November 19, 2001  Last Modified: November 20, 2001

Communities

AppServer  |  C++  |  CORBA  |  Delphi  |  InterBase  |  Java  |  Linux

Books |  Chat |  Code Central |  Downloads |  Feedback
Help |  Home Pages |  Museum |  Newsgroups |  Shopping